//
//  Joystick.hpp
//  Clock Signal
//
//  Created by Thomas Harte on 14/10/2017.
//  Copyright 2017 Thomas Harte. All rights reserved.
//

#pragma once

#include <cstddef>
#include <vector>

namespace Inputs {

/*!
	Provides an intermediate idealised model of a simple joystick, allowing a host
	machine to toggle states, while an interested party either observes or polls.
*/
class Joystick {
public:
	virtual ~Joystick() = default;

	/*!
		Defines a single input, any individually-measured thing — a fire button or
		other digital control, an analogue axis, or a button with a symbol on it.
	*/
	struct Input {
		/// Defines the broad type of the input.
		enum Type {
			// Half-axis inputs.
			Down, Up, Left, Right,
			// Full-axis inputs.
			Horizontal, Vertical,
			// Fire buttons.
			Fire,
			// Other labelled keys.
			Key,

			// The maximum value this enum can contain.
			Max = Key
		};
		const Type type;

		bool is_digital_axis() const {
			return type < Type::Horizontal;
		}
		bool is_analogue_axis() const {
			return type >= Type::Horizontal && type < Type::Fire;
		}
		bool is_axis() const {
			return type < Type::Fire;
		}
		bool is_button() const {
			return type >= Type::Fire;
		}

		enum Precision {
			Analogue, Digital
		};
		Precision precision() const {
			return is_analogue_axis() ? Precision::Analogue : Precision::Digital;
		}

		/*!
			Holds extra information pertaining to the input.

			@c Type::Key inputs declare the symbol printed on them.

			All other types of input have an associated index, indicating whether they
			are the zeroth, first, second, third, etc of those things. E.g. a joystick
			may have two fire buttons, which will be buttons 0 and 1.
		*/
		union Info {
			struct {
				size_t index;
			} control;
			struct {
				wchar_t symbol;
			} key;
		};
		Info info;
		// TODO: Find a way to make the above safely const; may mean not using a union.

		Input(const Type type, const size_t index = 0) :
			type(type) {
			info.control.index = index;
		}
		Input(const wchar_t symbol) : type(Key) {
			info.key.symbol = symbol;
		}

		bool operator == (const Input &rhs) {
			if(rhs.type != type) return false;
			if(rhs.type == Key) {
				return rhs.info.key.symbol == info.key.symbol;
			} else {
				return rhs.info.control.index == info.control.index;
			}
		}
	};

	/// @returns The list of all inputs defined on this joystick.
	virtual const std::vector<Input> &get_inputs() = 0;

	/*!
		Sets the digital value of @c input. This may have direct effect or
		influence an analogue value; e.g. if the caller declares that ::Left is
		active but this joystick has only an analogue horizontal axis, this will
		cause a change to that analogue value.
	*/
	virtual void set_input(const Input &input, bool is_active) = 0;

	/*!
		Sets the analogue value of @c input. If the input is actually digital,
		or if there is a digital input with a corresponding meaning (e.g. ::Left
		versus the horizontal axis), this may cause a digital input to be set.

		@c value should be in the range [0.0, 1.0].
	*/
	virtual void set_input(const Input &input, float value) = 0;

	/*!
		Sets all inputs to their resting state.
	*/
	virtual void reset_all_inputs() {
		for(const auto &input: get_inputs()) {
			if(input.precision() == Input::Precision::Digital)
				set_input(input, false);
			else
				set_input(input, 0.5f);
		}
	}

	/*!
		Gets the number of input fire buttons.

		This is cached by default, but it's virtual so overridable.
	*/
	virtual int get_number_of_fire_buttons() {
		if(number_of_buttons_ >= 0) return number_of_buttons_;

		number_of_buttons_ = 0;
		for(const auto &input: get_inputs()) {
			if(input.type == Input::Type::Fire) ++number_of_buttons_;
		}
		return number_of_buttons_;
	}

private:
	int number_of_buttons_ = -1;
};

/*!
	ConcreteJoystick is the class that it's expected most machines will actually subclass;
	it accepts a set of Inputs at construction and thereby is able to provide the
	promised analogue <-> digital mapping of Joystick.
*/
class ConcreteJoystick: public Joystick {
public:
	ConcreteJoystick(const std::vector<Input> &inputs) : inputs_(inputs) {
		// Size and populate stick_types_, which is used for digital <-> analogue conversion.
		for(const auto &input: inputs_) {
			const bool is_digital_axis = input.is_digital_axis();
			const bool is_analogue_axis = input.is_analogue_axis();
			if(is_digital_axis || is_analogue_axis) {
				const size_t required_size = size_t(input.info.control.index+1);
				if(sticks_.size() < required_size) {
					sticks_.resize(required_size);
				}
				sticks_[size_t(input.info.control.index)].type =
					is_digital_axis ? Stick::Type::Digital : Stick::Type::Analogue;
			}
		}
	}

	const std::vector<Input> &get_inputs() final {
		return inputs_;
	}

	void set_input(const Input &input, const bool is_active) final {
		// If this is a digital setting to a digital property, just pass it along.
		if(input.is_button() || sticks_[input.info.control.index].type == Stick::Type::Digital) {
			did_set_input(input, is_active);
			return;
		}

		// Otherwise this is logically to an analogue axis; map appropriately.
		// TODO: make these a function of time.
		auto &stick = sticks_[input.info.control.index];
		stick.apply_digital(input, is_active);
		const auto analogue_value = [&](const int mask) {
			switch(mask) {
				default:	return 0.5f;
				case 0b01:	return digital_minimum();
				case 0b10:	return digital_maximum();
			}
		};

		switch(input.type) {
			using enum Joystick::Input::Type;

			default:
				did_set_input(input, is_active ? 1.0f : 0.0f);
			break;

			case Left:
			case Right:
				did_set_input(
					Input(Horizontal, input.info.control.index),
					analogue_value(stick.digital_mask(Horizontal))
				);
			break;
			case Up:
			case Down:
				did_set_input(
					Input(Vertical, input.info.control.index),
					analogue_value(stick.digital_mask(Vertical))
				);
			break;
		}
	}

	void set_input(const Input &input, const float value) final {
		// If this is an analogue setting to an analogue property, just pass it along.
		if(!input.is_button() && sticks_[input.info.control.index].type == Stick::Type::Analogue) {
			did_set_input(input, value);
			return;
		}

		// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
		using Type = Joystick::Input::Type;
		switch(input.type) {
			default:
				did_set_input(input, value > 0.5f);
			break;
			case Type::Horizontal:
				did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
				did_set_input(Input(Type::Right, input.info.control.index), value >= 0.75f);
			break;
			case Type::Vertical:
				did_set_input(Input(Type::Up, input.info.control.index), value <= 0.25f);
				did_set_input(Input(Type::Down, input.info.control.index), value >= 0.75f);
			break;
		}
	}

protected:
	virtual void did_set_input([[maybe_unused]] const Input &input, [[maybe_unused]] float value) {}
	virtual void did_set_input([[maybe_unused]] const Input &input, [[maybe_unused]] bool value) {}
	virtual float digital_minimum() const { return 0.1f; }
	virtual float digital_maximum() const { return 0.9f; }

private:
	const std::vector<Input> inputs_;

	struct Stick {
		enum class Type {
			Digital,
			Analogue
		} type;

		void apply_digital(const Input &input, const bool is_active) {
			const int mask = [&] {
				switch(input.type) {
					default: return 0;
					case Input::Type::Up:		return 1 << 0;
					case Input::Type::Down:		return 1 << 1;

					case Input::Type::Left:		return 1 << 2;
					case Input::Type::Right:	return 1 << 3;
				}
			} ();
			if(is_active) {
				digital_inputs_ |= mask;
			} else {
				digital_inputs_ &= ~mask;
			}
		}
		int digital_mask(const Input::Type axis) const {
			switch(axis) {
				default: return 0;
				case Input::Type::Horizontal:	return (digital_inputs_ >> 2) & 3;
				case Input::Type::Vertical:		return (digital_inputs_ >> 0) & 3;
			}
		}
		int digital_inputs_ = 0;
	};
	std::vector<Stick> sticks_;
};

}
